CS = (typeof CS != "undefined") ? CS : { };

CS.Log = {warn: function(msg){}, error: function(msg){ return false; }};

CS.GetContext = function(canvasEl) {

  var gl = false;
  var webglcanvases = ["webgl", "moz-webgl", "webkit-3d", "expermiental-webgl"];
  for (i in webglcanvases) {
    try {
      gl = canvasEl.getContext(webglcanvases[i]);
    } catch (e) { }
    if (gl) {
      break;
    }
  }

  if (!gl) {
    CS.Log.error("Can't find a WebGL context; is it enabled?");
    return null;
  }

  /*if(WebGLDebugUtils) {
    gl= WebGLDebugUtils.makeDebugContext(gl);
  }*/

  gl.viewportWidth = canvasEl.width;
  gl.viewportHeight = canvasEl.height;

  return gl;
};


CS.HasWebGL = function() {
  return (CS.GetContext(document.createElement('canvas')) !== null);
};

CS.hasWebGLSupport = CS.HasWebGL();

/**
 *  The default URL resolver does nothing special, just returns passed URL.
 */
CS.DefaultURLResolver = function() { };
CS.DefaultURLResolver.prototype.Resolve = function(assetid, callback) {
  callback(url);
};

CS.XMLNode = function(node, xml) {

  this.node = node;
  this.xml = xml;

  this.nsResolver = function (prefix) {
    var ns = {
      'c' : 'http://crystalspace.org/xml/library'
    };
    return ns[prefix] || null;
  };

  this.GetNode = function (xpathexpr) {
    var x = this.xml;
    if (x == null) {
      x = this.node;
    }
    var result = null;
    try {
     result = x.evaluate(xpathexpr, this.node, this.nsResolver, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    } catch (e) {
      CS.Log.error("GetNode: "+xpathexpr+', '+this.xml+': '+e);
    }
    if (result) {
      return new CS.XMLNode(result, x);
    }
    return null;
  };

  this.GetNodesIterator = function (xpathexpr) {
    var x = this.xml;
    if (x == null) {
      x = this.node;
    }
    var result = null;
    try {
     result = x.evaluate(xpathexpr, this.node, this.nsResolver, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
    } catch (e) { alert(e); CS.Log.error("GetNodesIterator: "+e); }
    return result;
  };

  this.GetNodes = function (xpathexpr) {
    var x = this.xml;
    if (x == null) {
      x = this.node;
    }
    var result = null;
    try {
     result = x.evaluate(xpathexpr, this.node, this.nsResolver, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
    } catch (e) { alert(e); CS.Log.error("GetNodes: "+e); }
    var nodes = [];
    var n = result.iterateNext();
    while (n)
    {
      nodes.push(new CS.XMLNode(n, x));
      n = result.iterateNext();
    }
    return nodes;
  };

  this.GetText = function() {
    var s = "";
    for (c = this.node.firstChild; c; c = c.nextSibling) {
      if (c.nodeType != 3) {
        break;
      }
      s += c.textContent;
    }
    return s;
  };

  this.GetAttribute = function(name) {
    return this.node.getAttribute(name);
  };

};

CS.runSoon = function(f) {
  setTimeout(f, 0);
};

CS.Loader = function(engine) {
  this.Engine = engine;
  this.prefix = '';

  this.ImageURLResolver = new CS.DefaultURLResolver();

  this.LoadTexture = function(node) {
    var gl = this.Engine.gl;

    var self = this;

    var file = node.GetNode('c:file').GetText();
    var name = node.GetAttribute('name');

    var texture = gl.createTexture();
          texture.image = new Image();

    // register a callback for errors, load a standard tex.
    function onerror() {
      texture.image.src = self.prefix+"/static/js/webgl/UV_mapper.jpg";
    }
    function onload () {
      gl.bindTexture(gl.TEXTURE_2D, texture);
      //gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.image);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
      gl.generateMipmap(gl.TEXTURE_2D);
      gl.bindTexture(gl.TEXTURE_2D, null);
      CS.Log.warn("Init texture, size : "+texture.image.width+" x "+texture.image.height);
      texture.loaded = true;
      gl.uniform1i(self.Engine.RenderManager.shaderProgram.useTexturesUniform, true);
      self.Engine.RenderManager.Frame();
      self.Engine.RenderManager.Frame();//TODO: need to call it twice or it stays black wtf?
    }
    texture.image.onerror = onerror;
    texture.image.onload = onload;
    texture.image.name = name;
    self.ImageURLResolver.Resolve(file, function(src) {
      texture.image.src = src;
    });

    this.Engine.textures[name] = texture;

    return name;
  };

  this.LoadMaterial = function(node) {
    var name = node.GetAttribute('name');
    var textures = {};

    this.Engine.materials[name] = {name: name, textures: textures};

    return name;
  };

  this.LoadFact = function(node) {
    var name = node.GetAttribute('name');
    var plugin = node.GetNode('//c:plugin');
    plugin = plugin.GetText().split('.');
    plugin = plugin[plugin.length-1];

    if (CS.Loaders[plugin]) {
      var fact = new CS.MeshFactoryWrapper(this.Engine, name);
      var params = node.GetNode('//c:params');
      CS.Loaders[plugin](this, fact, params);
      this.Engine.factories[name] = fact;
    }
    else {
      CS.Log.error("LoadLibrary: failed! Fact: "+name);
    }

    return name;
  };

  this.LoadLibraryDOM = function (document, onLoadCompleted) {

    var i;
    var name;
    var self = this;
    var root = new CS.XMLNode(document);

    var textures = root.GetNode('//c:library/c:textures');
    textures = textures.GetNodes('//c:texture');
    for (i = 0; i < textures.length; i++) {
      name = self.LoadTexture(textures[i]);
      CS.Log.error("LoadLibrary: Texture: "+name);
    }

    var materials = root.GetNode('//c:library/c:materials');
    materials = materials.GetNodes('//c:material');
    for (i = 0; i < materials.length; i++) {
      name = self.LoadMaterial(materials[i]);
      CS.Log.error("LoadLibrary: Material: "+name);
    }

    var facts = root.GetNodes('//c:library/c:meshfact');
    for (i = 0; i < facts.length; i++) {
      name = self.LoadFact(facts[i]);
      CS.Log.error("LoadLibrary: Factory: "+name);
    }

    if (onLoadCompleted) {
      CS.runSoon(onLoadCompleted);
    }
  };

  this.LoadLibrary = function(url, onLoadCompleted) {
    var xhr = new XMLHttpRequest();
    var self = this;
    xhr.onreadystatechange = function () {
      if (xhr.readyState == 4 && xhr.status == 200) {
        CS.runSoon(function () {
          self.LoadLibraryDOM(xhr.responseXML, onLoadCompleted);
        });
      }
    };

    var normurl;
    if (url.substring(0, 7) == 'http://') {
      normurl = url;
    }
    else {
      normurl = self.prefix + url;
    }
    xhr.open("GET", normurl, true);
    xhr.overrideMimeType("text/xml");
    //xhr.setRequestHeader("Content-Type", "text/xml");
    xhr.send(null);
  };

  this.ParseRenderBuffer = function (node, fname) {
    var components = parseInt(node.GetAttribute('components'), 10);

    var typeStr = node.GetAttribute('type');
    var type = null;
    if (typeStr == "float") {
      type = parseFloat;
    }
    else if (typeStr == "uint") {
      type = parseInt;
    }
    else {
      CS.Log.error("ParseRenderBuffer: failed: "+fname);
      return null;
    }

    var res = Array();
    var iterator = node.GetNodesIterator('c:e');

    var thisNode = iterator.iterateNext();
    while (thisNode)
    {
      var i = [0, 1, 2];
      if (fname == 'position') {
        i = [0, 2, 1];
      }
      for (var c = 0; c < components; c++)
      {
        res.push( type(thisNode.getAttribute('c'+i[c])) );
      }
      thisNode = iterator.iterateNext();
    }
    return {type: typeStr, components: components, data: res};
  };

  this.LoadShader = function (id) {
    var shaderScript = document.getElementById(id);
    if (!shaderScript) {
      return null;
    }
    var str = "";
    var k = shaderScript.firstChild;
    while (k) {
      if (k.nodeType == 3) {
        str += k.textContent;
      }
      k = k.nextSibling;
    }
    var shader;
    if (shaderScript.type == "x-shader/x-fragment") {
      shader = this.Engine.gl.createShader(this.Engine.gl.FRAGMENT_SHADER);
    }
    else if (shaderScript.type == "x-shader/x-vertex") {
      shader = this.Engine.gl.createShader(this.Engine.gl.VERTEX_SHADER);
    }
    else {
      return null;
    }
    this.Engine.gl.shaderSource(shader, str);
    this.Engine.gl.compileShader(shader);
    if (!this.Engine.gl.getShaderParameter(shader, this.Engine.gl.COMPILE_STATUS)) {
      CS.Log.error("LoadShader:"+this.Engine.gl.getShaderInfoLog(shader));
      return null;
    }
    return shader;
  };

};

CS.Engine = function(canvasEl) {
  this.canvasEl = canvasEl;
  this.gl = null;

  this.Loader = null;
  this.RenderManager = null;

  this.textures = {};
  this.materials = {};
  this.factories = {};
  this.objects = {};

  this.Init = function() {
    if (this.canvasEl.Engine) {
      this.canvasEl.Engine.Clear();
    }
    this.canvasEl.Engine = this;
    this.Loader = new CS.Loader(this);
    this.RenderManager = new CS.RenderManager(this);

    this.gl = CS.GetContext(this.canvasEl);
    if (!this.gl) {
      return CS.Log.error("CS.GetContext failed!");
    }
    var r = this.RenderManager.InitShaders();
    if (!r) {
      return CS.Log.error("CS.RenderManager.InitShaders failed!");
    }
    this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
    this.gl.clearDepth(1.0);
           this.gl.enable(this.gl.DEPTH_TEST);
    this.gl.depthFunc(this.gl.LEQUAL);
    return true;
  };

  this.Run = function () {
    this.RenderManager.Frame();
  };

  this.CreateMeshWrapper = function(name, factory, position) {
    var o =  new CS.MeshWrapper(name, factory, position);
    this.objects[name] = o;
    return o;
  };

  this.Prepare = function () {
    CS.Log.warn("CS.Engine.Prepare!");
    for (var f in this.factories) {
      this.factories[f].InitBuffers();
    }
  };

  this.Clean = function () {
    for (var f in this.factories) {
      this.factories[f].DestroyBuffers();
    }
  };

  this.LoadLibraryDOM = function (document, prefix) {
    var self = this;
    if (prefix) {
      self.Loader.prefix = prefix;
    }
    self.Loader.LoadLibraryDOM(document, function () {
      self.Prepare();
      for (var f in self.factories) {
        var fact = self.factories[f];
        self.CreateMeshWrapper('test', fact , [0.0, 0.0, 0.0]);
      }
      self.RenderManager.InitCamera();
      CS.runSoon(self.Run.bind(self));
    });
  };

  this.LoadLibrary = function (url, prefix) {
    var self = this;
    if (prefix) {
      self.Loader.prefix = prefix;
    }
    self.Loader.LoadLibrary(url,
      function () {
        self.Prepare();
        for (var f in self.factories) {
          var fact = self.factories[f];
          self.CreateMeshWrapper('test', fact , [0.0, 0.0, 0.0]);
        }
        self.RenderManager.InitCamera();
        CS.runSoon(self.Run.bind(self));
      });
  };

  this.InitBuffers = function(object) {
    for (var b in object.renderbuffers) {
      CS.Log.error("Engine: InitBuffers: buffer:"+b+object.renderbuffers[b]);
      var ArrayType = null;
      var ARRAY = null;
      if (object.renderbuffers[b].type == 'uint') {
        ArrayType = Uint16Array;
        ARRAY = this.gl.ELEMENT_ARRAY_BUFFER;
      }
      else if (object.renderbuffers[b].type == 'float') {
        ArrayType = Float32Array;
        ARRAY = this.gl.ARRAY_BUFFER;
      }
      else {
        CS.Log.error("Engine: InitBuffers: Failed!"+b+', '+object.renderbuffers[b].type);
        continue;
      }
      object.glbuffers[b] = this.gl.createBuffer();
            this.gl.bindBuffer(ARRAY, object.glbuffers[b]);
            this.gl.bufferData(ARRAY, new ArrayType(object.renderbuffers[b].data), this.gl.STATIC_DRAW);
      object.glbuffers[b].itemSize = object.renderbuffers[b].components;
      object.glbuffers[b].numItems = (object.renderbuffers[b].data.length)/object.renderbuffers[b].components;
      //finally delete raw data
      delete object.renderbuffers[b];
    }
  };

  this.DestroyBuffers = function(object) {
    for (var b in object.glbuffers) {
      if (object.glbuffers[b]) {
        this.gl.deleteBuffer(object.glbuffers[b]);
      }
    }
  };
};


CS.MeshFactoryWrapper = function(engine, name) {
  this.Engine = engine;
  this.name = name;
  this.renderbuffers = {};
  this.glbuffers = {};
  this.submeshes = [];
  this.material = null;
  this.init = false;

  this.InitBuffers = function() {
    if (this.init) {
      return;
    }
    CS.Log.warn("MeshFactoryWrapper: InitBuffers: "+this.name);
    this.Engine.InitBuffers(this);
    for (var s in this.submeshes) {
      this.Engine.InitBuffers(this.submeshes[s]);
    }
    this.init = true;
  };

  this.DestroyBuffers = function() {
    CS.Log.warn("MeshFactoryWrapper: DestroyBuffers: "+this.name);
    if (!this.init) {
      return;
    }
    this.Engine.DestroyBuffers(this);
    for (var s in this.submeshes) {
      this.Engine.DestroyBuffers(this.submeshes[s]);
    }
    this.init = false;
  };
};

CS.MeshFactoryWrapper.Submesh = function(factory) {
  factory.submeshes.push(this);
  this.factory = factory;
  this.material = null;
  this.renderbuffers = {};
  this.glbuffers = {};
  this.shadervars = {};
};

CS.MeshWrapper = function(name, factory, position) {
  this.name = name;
  this.factory = factory;
  this.position = position;
};
